package org.nViron.nvOrganic;

import java.util.ArrayList;
import java.util.Iterator;

import org.nViron.nvData.nvDataEntity;
import org.nViron.nvOrganism.nvStateMgr;
import org.nViron.nvParams.nvContextParamMgr.CONTEXTPARAM;
import org.nViron.nvParams.nvVizCalculator;
import org.nViron.nvParams.nvVizParamMgr.VIZPARAM;
import org.nViron.nvParams.nvVizValueCache;
import org.nViron.nvRender.nvRenderer;
import org.nViron.nvRender.nvVec3;

import traer.physics.Vector3D;

/**
 * A node is connected by a stem to another node (or root). It holds data entities. Data entities can sprout from it as another stem or as leaves on stalks or regress.
 * @author Roland Haring <p>
 * <script type="text/javascript" src="http://www.ohloh.net/p/376434/widgets/project_basic_stats.js"></script>
 */
public class nvNode extends nvKinetic implements nvVizCalculator {

    private final ArrayList unassignedEntities = new ArrayList();
    private final ArrayList keptEntities = new ArrayList();
    private final ArrayList stalks = new ArrayList();
    private final ArrayList stems = new ArrayList();
    private final nvVizValueCache vizCache = new nvVizValueCache(this);
    private float sproutThreshold = 0.5f; // TODO make this dependent on a VIZVALUE
    private int maxNumStems;
    private int maxLevel = 3;
    private int stalkVal = 3;
    private int count;
    private long nextUnkeepStamp;
    private int unKeepInterval = 5000; // unassign kept entities every 5000 +/- 50% msec
    private boolean doDegenerate = false;

    public nvNode(nvOrganic organic, nvStem parent) {
        super(organic, parent);
        updateAllVizValues();
        float c1Val = 20.0f + (5 - level > 1 ? (5.0f - level) * 10.0f * (float) Math.random() : 0.0f); // TODO make this dependent on a VIZVALUE
        c1Val += c1Val / 2.0f * Math.random() - c1Val / 4.0f; // some random +/- 25%
        c1 = getOrganismMgr().getSimulator().getProjection().geoToVizApprox(c1Val);
        c2 = getOrganismMgr().getSimulator().getProjection().geoToVizApprox(40.0f); // TODO make this dependent on a VIZVALUE
        maxNumStems = (int) Math.ceil(Math.random() * 2.0f) + level; // TODO make this dependent on a VIZVALUE
        if (maxNumStems > 4) { // heuristic
            maxNumStems = 4;
        }
    }

    @Override
    public void addEntity(nvDataEntity entity) {
        unassignedEntities.add(entity);
    }

    protected boolean biodiversify() {
        boolean ret = false;
        // create new stems and assign some of the entities to them
        // is there is a new stem, then entity is removed from this entities list and added to that of the new stem!
        if (stems.size() >= maxNumStems || level >= maxLevel) {
            return false;
        }
        // count how many entities we have for new stems
        int numFittingStems[] = new int[maxNumStems];
        for (Iterator it = unassignedEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            float biodivVal = entity.getVizValue(getBiodiversityParam());
            int index = (int) Math.ceil(biodivVal * (maxNumStems - 1));
            numFittingStems[index]++;
        }
        // check if we found at least two for a new stem
        for (int i = 0; i < numFittingStems.length; i++) {
            if (numFittingStems[i] >= 2) {
                nvStem stem = new nvStem(getOrganic(), this);
                stem.setRipening((float) i / (float) maxNumStems, (float) (i + 1) / (float) maxNumStems, getBiodiversityParam());
                stems.add(stem);
                ret = true;
            }
        }
        if (ret) {
            ripen();
        }
        return ret;
    }

    public float calculateAverageVizValue(VIZPARAM vizParam) {
        float sum = 0.0f;
        int numEntities = 0;
        for (Iterator i = stalks.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            sum += joint.getVizValue(vizParam) * joint.getNumEntities();
            numEntities += joint.getNumEntities();
        }
        for (Iterator i = stems.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            sum += joint.getVizValue(vizParam) * joint.getNumEntities();
            numEntities += joint.getNumEntities();
        }
        for (Iterator it = keptEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            sum += entity.getVizValue(vizParam);
            numEntities++;
        }
        for (Iterator it = unassignedEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            sum += entity.getVizValue(vizParam);
            numEntities++;
        }
        if (numEntities == 0) {
            return 0.0f;
        }
        return sum / numEntities;
    }

     public float calculateVizValue(VIZPARAM param) {
        // TODO think of more sophisticated ways on how to do this (use maximum instead?)
        return calculateAverageVizValue(param);
    }

    @Override
    public void degenerate() {
        doDegenerate = true;
        for (Iterator i = stems.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            joint.degenerate();
        }
        for (Iterator i = stalks.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            joint.degenerate();
        }
        if (stems.isEmpty() && stalks.isEmpty() && getState() != nvStateMgr.DEAD) {
            setState(nvStateMgr.DEGENERATING);
        }
    }

    @Override
    protected boolean doneDegenerating() {
        return (particle == null || particle.position().distanceTo(connectingJoint.getSource().getParticle().position()) <= getC1() / 10.0f);
    }

    @Override
    protected boolean doneSprouting() {
        boolean bRet = particle.isFree() && !particle.isAttractionInfluenced() && !particle.isSpringInfluenced() && particle.position().distanceTo(connectingJoint.getSource().getParticle().position()) >= getC1();
        if (bRet) {
            // System.out.println("SPROUT STOP at " + particle.position().toString());
        }
        return bRet;
    }

    protected VIZPARAM getBiodiversityParam() {
        // use depending on level differenz biodiversity value
        VIZPARAM vizparam = VIZPARAM.INVALID;
        switch (level) {
            case 1:
                vizparam = VIZPARAM.BIODIVERSITY_L1;
                break;
            case 2:
                vizparam = VIZPARAM.BIODIVERSITY_L2;
                break;
            case 3:
                vizparam = VIZPARAM.BIODIVERSITY_L3;
                break;
        }
        return vizparam;
    }

    @Override
    public nvVec3 getCurrentRenderPos() {
        if (particle == null) {
            return connectingJoint.getSource().getCurrentRenderPos(); // not moving yet
        }
        nvVec3 pos = connectingJoint.getSource().getCurrentRenderPos();
        // TODO this is a bit of a hack now! (our position is not relative to parent but relative to root)
        pos.x += particle.position().x() - connectingJoint.getSource().getParticle().position().x();
        pos.y += particle.position().y() - connectingJoint.getSource().getParticle().position().y();
        pos.z += particle.position().z() - connectingJoint.getSource().getParticle().position().z();
        return pos;
    }

    @Override
    public Vector3D getInitialVelocity() {
        Vector3D ret = new Vector3D();
        // TODO make this more intelligent and use stems ripenMin and ripenMax to make structure more biased!
        ret.setX(((float) Math.random() - 0.5f) / 5.0f / (getLevel() == 1 ? 5.0f : 1.0f)); // TODO make this dependent on some VIZPARAM
        ret.setZ(((float) Math.random() - 0.5f) / 5.0f / (getLevel() == 1 ? 5.0f : 1.0f)); // TODO make this dependent on some VIZPARAM
        ret.setY(((float) Math.random() * 0.25f + 0.75f) * getOrganismMgr().getSimulator().getProjection().geoToVizApprox(100.0f)); // TODO make this dependent on some VIZPARAM
        return ret;
    }

    public ArrayList getKeptEntities() {
        return keptEntities;
    }

    @Override
    protected int getNumChildren() {
        return stems.size() + stalks.size();
    }

    @Override
    public int getNumEntities() {
        int ret = 0;
        ret += unassignedEntities.size();
        ret += keptEntities.size();
        for (Iterator i = stalks.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            ret += joint.getNumEntities();
        }
        for (Iterator i = stems.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            ret += joint.getNumEntities();
        }
        return ret;
    }

    public float getSimilarityTo(nvDataEntity other) {
        // return average of all containing entities
        float sum = 0.0f;
        int num = 0;
        for (Iterator i = stalks.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            sum += joint.getSimilarityTo(other);
            num += joint.getNumEntities();
        }
        for (Iterator i = stems.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            sum += joint.getSimilarityTo(other);
            num += joint.getNumEntities();
        }
        for (Iterator it = keptEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            sum += entity.getSimilarityTo(other);
            num++;
        }
        for (Iterator it = unassignedEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            sum += entity.getSimilarityTo(other);
            num++;
        }
        return (num == 0 ? 0 : sum / num);
    }

    public float getSimilarityTo(nvDataEntity other, CONTEXTPARAM contextParam) {
        // return average of all containing entities
        float sum = 0.0f;
        int num = 0;
        for (Iterator i = stalks.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            sum += joint.getSimilarityTo(other, contextParam);
            num += joint.getNumEntities();
        }
        for (Iterator i = stems.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            sum += joint.getSimilarityTo(other, contextParam);
            num += joint.getNumEntities();
        }
        for (Iterator it = keptEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            sum += entity.getSimilarityTo(other, contextParam);
            num++;
        }
        for (Iterator it = unassignedEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            sum += entity.getSimilarityTo(other, contextParam);
            num++;
        }
        return (num == 0 ? 0 : sum / num);
    }

    public float getSimilarityTo(nvDataEntity other, VIZPARAM vizParam) {
        // return average of all containing entities
        float sum = 0.0f;
        int num = 0;
        for (Iterator i = stalks.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            sum += joint.getSimilarityTo(other, vizParam);
            num += joint.getNumEntities();
        }
        for (Iterator i = stems.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            sum += joint.getSimilarityTo(other, vizParam);
            num += joint.getNumEntities();
        }
        for (Iterator it = keptEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            sum += entity.getSimilarityTo(other, vizParam);
            num++;
        }
        for (Iterator it = unassignedEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            sum += entity.getSimilarityTo(other, vizParam);
            num++;
        }
        return (num == 0 ? 0 : sum / num);
    }

    public ArrayList getStalks() {
        return stalks;
    }

    public ArrayList getStems() {
        return stems;
    }

    public ArrayList getUnassignedEntities() {
        return unassignedEntities;
    }

    @Override
    public float getVizValue(VIZPARAM param) {
        return vizCache.getVizValue(param);
    }

    @Override
    public boolean isRenderable() {
        return super.isRenderable();
    }

    @Override
    protected void onStateChange(int state) {
        switch (state) {
            case nvStateMgr.SPROUTING:
                particle = getOrganic().generateParticle(this);
                particle.position().set(connectingJoint.getSource().getParticle().position());
                break;
            case nvStateMgr.LIVING:
                particle.makeFixed();
                getOrganic().getParticleSystem().removeSprings(particle);
                getOrganic().getParticleSystem().removeAttractions(particle);
                break;
            case nvStateMgr.DEGENERATING:
                if (particle != null) {
                    particle.velocity().clear();
                    getOrganic().getParticleSystem().removeAttractions(particle);
                    getOrganic().getParticleSystem().removeSprings(particle);
                    particle.makeFree();
                    getOrganic().getParticleSystem().makeSpring(particle, connectingJoint.getSource().getParticle(), 10.0f, 10.0f, 0.0f);
                }
                break;
        }
    }

    protected boolean regress() {
        boolean ret = false;
        // TODO implement
        // remove kept entities from stems that are not needed there anymore
        // remove stems and sprouts that are not needed anymore,
        // if they have entities then they need to be added to the node's unassignedEntity list
        // 1. remove sprouts, when there are stems and assign entities to unassigned entities!
        // 2. regarding stem: check the BIODIVERSITY_Lx values! These values can change during runtime -> there could be stems with identical or similar ones -> one needs to be remove then
        //    -> use stem's canRipen to check kept and unassigned entities of children
        // also check if stalk could ripen as well. if so remove the stalk and let the sprout ripen
        if (stems.size() > 0 && stalks.size() > 0) {
            for (Iterator i = stalks.iterator(); i.hasNext();) {
                nvStalk stalk = (nvStalk) i.next();
                if (stalk.getLeaf().hasEntity()) {
                    addEntity(stalk.getLeaf().getEntity());
                    stalk.getLeaf().dropEntity();
                }
                stalk.degenerate();
            }
        }

        return ret;
    }

    @Override
    public void remove() {
        for (Iterator i = stalks.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            joint.remove();
        }
        for (Iterator i = stems.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            joint.remove();
        }
        super.remove();
    }

    @Override
    public boolean removeEntity(nvDataEntity entity) {
        for (Iterator i = stems.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            if (joint.removeEntity(entity)) {
                if (getNumEntities() == 0) {
                    degenerate();
                } else {
                    updateAllVizValues();
                }
                return true;
            }
        }
        for (Iterator i = stalks.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            if (joint.removeEntity(entity)) {
                if (getNumEntities() == 0) {
                    degenerate();
                } else {
                    updateAllVizValues();
                }
                return true;
            }
        }
        for (Iterator it = keptEntities.iterator(); it.hasNext();) {
            nvDataEntity myEntity = (nvDataEntity) it.next();
            if (myEntity.getId() == entity.getId()) {
                it.remove();
                if (getNumEntities() == 0) {
                    degenerate();
                } else {
                    updateAllVizValues();
                }
                return true;
            }
        }
        for (Iterator it = unassignedEntities.iterator(); it.hasNext();) {
            nvDataEntity myEntity = (nvDataEntity) it.next();
            if (myEntity.getId() == entity.getId()) {
                it.remove();
                if (getNumEntities() == 0) {
                    degenerate();
                } else {
                    updateAllVizValues();
                }
                return true;
            }
        }
        return false;
    }

    public void render(nvRenderer renderer) {
        if (!isRenderable()) {
            return;
        }
        renderer.renderNode(this);
        for (Iterator i = stems.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            joint.render(renderer);
        }
        for (Iterator i = stalks.iterator(); i.hasNext();) {
            nvJoint joint = (nvJoint) i.next();
            joint.render(renderer);
        }
    }

    protected boolean ripen() {
        boolean ret = false;
        if (stems.isEmpty() || unassignedEntities.isEmpty()) {
            return ret;
        }
        // assign node's unassigned entities to existing stems based on similarity
        for (Iterator it = unassignedEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            for (Iterator i = stems.iterator(); i.hasNext();) {
                nvStem stem = (nvStem) i.next();
                if (stem.canRipen(entity)) {
                    it.remove();
                    stem.addEntity(entity);
                    break;
                }
            }
        }
        return ret;
    }

    public void selecet() {
        setSelection(false);
    }

    @Override
    protected void setSelection(boolean b) {
        super.setSelection(b);
        // recursivly
        Iterator it = stems.iterator();
        while (it.hasNext()) {
            nvJoint joint = (nvJoint) it.next();
            joint.setSelection(b);
        }
        it = stalks.iterator();
        while (it.hasNext()) {
            nvJoint joint = (nvJoint) it.next();
            joint.setSelection(b);
        }
    }

    private boolean shouldSprout(nvDataEntity entity) {
        // TODO add sophisticated sprout defining viz value evaluation here
        return entity.getVizValue(VIZPARAM.METABOLISM) > sproutThreshold;
    }

    protected boolean sprout() {
        boolean ret = false;
        if (stalks.size() > stalkVal || stems.size() > 0) {
            return false;
        }
        // create new stalks
        // remove sprouting entity form entity list
        for (Iterator it = unassignedEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            if (shouldSprout(entity)) {
                it.remove();
                nvStalk stalk = new nvStalk(getOrganic(), this, entity);
                stalks.add(stalk);
                ret = true;
            }
        }
        return ret;
    }

    private boolean unassignKeptEntities() {
        boolean ret = false;
        for (Iterator it = keptEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            unassignedEntities.add(entity);
            ret = true;
        }
        keptEntities.clear();
        return ret;
    }

    public void unselect() {
        setSelection(true);
    }

    @Override
    public void update(float timeStep) {
        super.update(timeStep);
        if (!doDegenerate && keptEntities.size() > 0 && nextUnkeepStamp < System.currentTimeMillis()) {
            unassignKeptEntities();
            nextUnkeepStamp += (int) (unKeepInterval * 0.5f + unKeepInterval * Math.random());
        }
        switch (getState()) {
            case nvStateMgr.SPROUTING:
                //   particle.velocity().setX(particle.velocity().x() + var * 0.5f - var * (float)Math.random());
                //   particle.velocity().setZ(particle.velocity().z() + var * 0.5f - var * (float)Math.random());
                break;
            case nvStateMgr.LIVING:
                if (stems.isEmpty() && stalks.isEmpty() && (doDegenerate || (unassignedEntities.isEmpty() && keptEntities.isEmpty()))) {
                    setState(nvStateMgr.DEGENERATING);
                    return;
                }
                // update stems
                for (Iterator it = stems.iterator(); it.hasNext();) {
                    nvStem stem = (nvStem) it.next();
                    stem.update(timeStep);
                    // remove dead ones
                    if (stem.getState() == nvStateMgr.DEAD) {
                        it.remove();
                        if (stem.getNumEntities() > 0) {
                            // copy remaining entities
                            for (Iterator it2 = stem.getNode().keptEntities.iterator(); it2.hasNext();) {
                                unassignedEntities.add(it2.next());
                                it2.remove();
                            }
                            for (Iterator it2 = stem.getNode().unassignedEntities.iterator(); it2.hasNext();) {
                                unassignedEntities.add(it2.next());
                                it2.remove();
                            }
                        }
                        stem.remove();
                    } else {
                        // sprout
                        if (getState() == nvStateMgr.LIVING && stem.getState() == nvStateMgr.SPAWN) {
                            stem.setState(nvStateMgr.SPROUTING);
                        }
                    }
                }
                // update stalks
                for (Iterator it = stalks.iterator(); it.hasNext();) {
                    nvStalk stalk = (nvStalk) it.next();
                    stalk.update(timeStep);
                    // remove dead one
                    if (stalk.getState() == nvStateMgr.DEAD) {
                        // remove stalk
                        it.remove();
                        // keep entity
                        if (stalk.getNumEntities() > 0) {
                            unassignedEntities.add(stalk.getLeaf().getEntity());
                            stalk.getLeaf().dropEntity();
                        }
                        stalk.remove();
                    } else {
                        // sprout
                        if (getState() == nvStateMgr.LIVING && stalk.getState() == nvStateMgr.SPAWN) {
                            stalk.setState(nvStateMgr.SPROUTING);
                        }
                    }
                }
                if (!doDegenerate) {
                    boolean changed = regress();
                    changed |= ripen();
                    changed |= biodiversify();
                    changed |= sprout();
                    changed |= updateKeptEntities();
                    if (changed) {
                        updateAllVizValues();
                    }
                }
            case nvStateMgr.DEGENERATING:
                break;
            case nvStateMgr.DEAD:
                break;
        }
        if (particle != null && particle.isFree()) {
            // System.out.println(particle.position().toString());
        }
    }

    protected final void updateAllVizValues() {
        vizCache.updateAllVizValues(getOrganismMgr().getSimulator().getVizParamMgr());
    }

    private boolean updateKeptEntities() {
        boolean ret = false;
        // TODO periodically unassign kept entities to give them a chance to move on to sprouts or stems, make this depending on vizparams
        count++;
        if (count == 30 * 5) { // every 5 seconds with 30 fps
            count = 0;
            for (Iterator it = keptEntities.iterator(); it.hasNext();) {
                nvDataEntity entity = (nvDataEntity) it.next();
                unassignedEntities.add(entity);
                it.remove();
            }
            return true;
        }
        // default: keep unassigned entities
        for (Iterator it = unassignedEntities.iterator(); it.hasNext();) {
            nvDataEntity entity = (nvDataEntity) it.next();
            keptEntities.add(entity);
            it.remove();
            ret = true;
        }
        return ret;
    }

    @Override
    public void updateVizValue(VIZPARAM param) {
        vizCache.updateVizValue(param);
    }
}
